#include "xmppcore/saslnegotiant.h"
#include "xmppcore/bindstreamfeature.h"
#include "xmppcore/sessionstreamfeature.h"
#include "xmppcore/connector.h"
#include "xmppcore/constants.h"

namespace yoohoo_xmppcore {

SaslNegotiant::SaslNegotiant(Connector* connector, QString userName, QString password,
			QString host, QString realm, QObject* parent) : Negotiant(connector, parent),_host(host),
				_realm(realm), _userName(userName), _password(password) {
	_saslNegotiationState = None;
	_sasl = 0;
	_waitCycles = 0;
	_socketDone = false;
	_streamFeatures = createStreamFeatures();
}

SaslNegotiant::~SaslNegotiant() {
	destroySasl();
}

void SaslNegotiant::negotiate() {
	if (!QCA::isSupported("sasl")) {
		emit saslError(SaslErrorNotSupported);
		return;
	}

	createSasl();
	QStringList mechlist;
	mechlist << "DIGEST-MD5";
	_sasl->startClient("xmpp", _host, mechlist);
}

void SaslNegotiant::createSasl() {
	if (_sasl == 0) {
		_sasl = new QCA::SASL;
		_sasl->setUsername(_userName);
		_sasl->setPassword(_password.toUtf8());
		if (_realm == "") {
			_realm = _host;
		}
		_sasl->setRealm(_realm);

		connect(_sasl, SIGNAL(clientStarted(bool, const QByteArray&)), this, SLOT(saslClientStarted(bool, const QByteArray&)));
		connect(_sasl, SIGNAL(nextStep(const QByteArray&)), this, SLOT(saslNextStep(const QByteArray&)));
		connect(_sasl, SIGNAL(needParams(const QCA::SASL::Params&)), this, SLOT(saslNeedParams(const QCA::SASL::Params&)));
		connect(_sasl, SIGNAL(error()), this, SLOT(processSaslError()));
		connect(_connector, SIGNAL(error(Connector::ConnectorError)), this, SLOT(connectorError(Connector::ConnectorError)));
	}
}

void SaslNegotiant::saslClientStarted(bool clientInit, const QByteArray& clientInitData) {
	qDebug("SaslNegotiant::saslClientStarted: Sasl client started");
	write("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>");
	_saslNegotiationState = AuthMechanismSent;
}

void SaslNegotiant::saslNextStep(const QByteArray& stepData) {
	qDebug("SaslNegotiant::saslNextStep: received a challenge[%s]", qPrintable(QString(stepData)));
	write(createResponseXml(stepData));
	_saslNegotiationState = ResponseSent;
}

QString SaslNegotiant::createResponseXml(const QByteArray& response) {
	QString base64Response = Base64().arrayToString(response);
	return QString("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%1</response>").arg(base64Response);
}

void SaslNegotiant::saslNeedParams(const QCA::SASL::Params& params) {
	qDebug("SaslNegotiant::saslNeedParams: need some parameters!!!");
}

void SaslNegotiant::processSaslError() {
	int e = _sasl->errorCode();
	if(e == SASL::ErrorInit) {
		emit saslError(SaslErrorInit);
	} else if(e == SASL::ErrorHandshake) {
		qDebug("SaslNegotiant::saslError: %s", qPrintable(saslAuthConditionToString(_sasl->authCondition())));
		emit saslError(SaslErrorHandshake);
	} else if(e == SASL::ErrorCrypt) {
		emit saslError(SaslErrorCrypt);
	} else {
		emit saslError(SaslErrorUnknown);
	}
}

QString SaslNegotiant::saslAuthConditionToString(QCA::SASL::AuthCondition x) {
	QString s;
	switch(x) {
	case QCA::SASL::NoMechanism:
		s = "no appropriate mechanism could be negotiated";
		break;
	case QCA::SASL::BadProtocol:
		s = "bad SASL protocol";
		break;
	case QCA::SASL::BadServer:
		s = "server failed mutual authentication";
		break;
	default:// AuthFail or unknown (including those defined for server only)
		s = "generic authentication failure";
	};

	return s;
}


void SaslNegotiant::destroySasl() {
	if (_sasl != 0) {
		delete _sasl;
	}
}

void SaslNegotiant::connectorReceived(QString xml) {
	if (xml.startsWith("<stream:error>")) // stream error will processed by 'Connection' class
		return;

	if (_saslNegotiationState == None || _saslNegotiationState == AbortedReceived ||
			_saslNegotiationState == ChallengeReceived) {
		qWarning("???Received a message in illegal sasl negotiation state???");
	} else if (_saslNegotiationState == AuthMechanismSent || _saslNegotiationState == ResponseSent) {
		if (xml == "<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>") {
			_saslNegotiationState = AuthenticationSuccess;
			emit success();
			Negotiant::negotiate();
			return;
		} else if (xml.startsWith("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>") &&
				xml.endsWith("</failure>")) {
			int start = 50;
			int end = xml.lastIndexOf("</failure");

			QString reason = xml.mid(start, (end - start));
			if (reason == "<incorrent-encoding/>"){
				emit saslError(SaslErrorIncorrectEncoding);
			} else if (reason == "<invalid-authzid/>"){
				emit saslError(SaslErrorInvalidAuthzid);
			} else if (reason == "<invalid-mechanism/>"){
				emit saslError(SaslErrorInvalidMechanism);
			} else if (reason == "<mechanism-too-weak/>"){
				emit saslError(SaslErrorMechanismTooWeak);
			} else if (reason == "<not-authorized/>"){
				emit saslError(SaslErrorNotAuthorized);
			} else if (reason == "<temporary-auth-failure/>"){
				emit saslError(SaslErrorTemporaryAuthFailure);
			} else {
				emit saslError(SaslErrorUnknown);
			}
		} else {
			QByteArray challenge;
			if (processChallenge(xml, challenge)) {
				_saslNegotiationState = ChallengeReceived;
				qDebug("SaslNegotiant::connectorReceived: received a challenge[%s]", qPrintable(QString(challenge)));
				_sasl->putStep(challenge);
			} else {
				_saslNegotiationState = AuthenticationError;
				emit negotiationError(UnknownNegotiationProtocolError);
			}
		}
	} else if (_saslNegotiationState == AbortSent) {
		if (xml != "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>") {
			_saslNegotiationState = AuthenticationError;
			emit negotiationError(UnknownNegotiationProtocolError);
		}
	} else if (_saslNegotiationState == AuthenticationSuccess) {
		Negotiant::connectorReceived(xml);
	} else {
		emit saslError(SaslErrorUnknown);
	}
}

void SaslNegotiant::processSocketError(QAbstractSocket::SocketError error) {
	if (error = QAbstractSocket::RemoteHostClosedError) {
		_socketDone = true;
	}
}

bool SaslNegotiant::processChallenge(QString& xml, QByteArray& challenge) {
	QXmlStreamReader reader(xml);
	reader.readNext(); // start document

	reader.readNext(); // start element
	if (reader.hasError() || !reader.isStartElement()) {
		return false;
	}

	if (reader.name().toString() != "challenge" || reader.prefix().toString() != "" ||
				reader.namespaceUri().toString() != XMPP_SASL_NAMESPACE) {
		return false;
	}

	reader.readNext();
	if (reader.hasError() || !reader.isCharacters()) {
		return false;
	}

	QString base64Challenge = reader.text().toString();
	challenge = Base64(QCA::Decode).stringToArray(base64Challenge).toByteArray();

	reader.readNext();
	if (reader.hasError() || !reader.isEndElement() ||
			reader.prefix().toString() != "" ||
			reader.name().toString() != "challenge" ||
			reader.namespaceUri().toString() != XMPP_SASL_NAMESPACE) {
		return false;
	}

	reader.readNext();
	if (reader.hasError() || !reader.isEndDocument()) {
		return false;
	}

	return true;
}

StreamFeatures* SaslNegotiant::createStreamFeatures() {
	StreamFeatures* streamFeatures = new StreamFeatures;
	streamFeatures->registerSupportedStreamFeature(new BindStreamFeature);
	streamFeatures->registerSupportedStreamFeature(new SessionStreamFeature);

	return new StreamFeatures;
}

}
